/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package Schiffeversenken;

import javax.bluetooth.*;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.io.StreamConnectionNotifier;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Calendar;
import java.util.Hashtable;
import java.util.Random;
import java.util.Vector;


/**
/**
 *
 * @author Bomidril
 * 
 * Established the BT service, accepts connections
 * and send the requested image silently.
 *
 * @version ,
 */
final class BTServer implements Runnable {
    /**
     * Describes this server
     */
    private static final UUID BATTLESHIPS_SERVER_UUID =
        new UUID ("F0E0D0C0B0A000908070605040302010", false);

    /**
     * Keeps the local device reference.
     */
    private LocalDevice localDevice;

    /**
     * Accepts new connections.
     */
    private StreamConnectionNotifier notifier;

    /**
     * Keeps the information about this server.
     */
    private ServiceRecord record;

    /**
     * Keeps the parent reference to process specific actions.
     */
    private GameInstance parent;

    /**
     * Becomes 'true' when this component is finalized.
     */
    private boolean isClosed;

    /**
     * Creates notifier and accepts clients to be processed.
     */
    private Thread accepterThread;

    /**
     * Process the particular client from queue.
     */
    private ClientProcessor processor;

    /**
     * Optimization: keeps the table of data elements to be published.
     */
    private final Hashtable dataElements = new Hashtable ();

    private StreamConnection connection;

    /** This state is set as normal gameplay is running */
    private static final int GAMEPLAY = 3;

    private int state = 0;
    // -------------------------------------------------------------------------
    public Battleground battleground;

    /** True if player has placed his ships and is ready to duel.
        Set this to true by calling PlayerReady().
     */
    private boolean playerReady = false;

    /** True if opponent has placed his ships and is ready to duel */
    private boolean opponentReady = false;

    /** ID of this game session */
    private String sessionId;

    /** This works as a semaphore to indicate whether outputString will be
     * written soon and is not free for read or write.
     */
    private boolean outputInUsageSemaphore = false;

    /*
     * Message Codes
     *
     * For each received SHOOT with a certain sequence number,
     * a device has to answer with the same sequence number if
     * its a shot into the WATER, a HIT, or SUNK.
     *
     * General setup:
     * MessageType : String +   // type of message (see codes below)
     * SessionID : String +     // a session unique ID
     * SequenceNumber : int (+  // ID of message
     * X-coord : int +          // optional
     * Y-coord : int)           // optional
     *
     * e.g: SHOOT+561+3+1  or  HIT+562+3+1  or  PLACED+1
     *
     * BTK + SessionId
     *          // other device is successfully set up and ready + new sessionID
     * RDY    // opponent has placed its ships and is waiting for other player
     * STC    // both players ready - client starts shooting
     * STS    // both players ready - server starts shooting
     * QIT    // player wants to leave the session or timeout
     *
     * SHT    // shot at given coords
     * WTR    // shot with according sequence number dropped into water
     * HIT    // shot with according sequence number hit an enemy ship
     * SNK    // shot with according sequence number hit and destroyed a ship
     *
     * not implemented but perhaps useful:
     * RAL    // request keepalive from other device after timeout of
     *        // unreceived message
     * ALV    // device is still connected
     * */

    public static final String BT_READY = "BTK";
    public static final String PLACEMENT_READY = "RDY";
    public static final String START_CLIENT = "STC";
    public static final String START_SERVER = "STS";
    public static final String QUIT = "QIT";

    public static final String SHOOT = "SHT";
    public static final String WATER = "WTR";
    public static final String HIT = "HIT";
    public static final String SUNK = "SNK";

    public static final String CONCAT = "+";

    /** Try to send a shoot message.
     * @return returns false if message cannot be sent currently, true otherwise
     */
    public boolean Shoot(Position pos)
    {
        /*if (this.state != GAMEPLAY  || this.isClosed ||
                this.outputWaiting || this.outputInUsageSemaphore ||
                !this.battleground.IsOnTurn())
        {
            return false;
        }*/
        this.outputInUsageSemaphore = true;

        // build the message
        String message = SHOOT +
                    CONCAT +
                this.sessionId  +
                    CONCAT +
                this.messageId +
                    CONCAT +
                String.valueOf(pos.X()) +
                    CONCAT +
                String.valueOf(pos.Y())
                ;

        boolean result = this.SendMessage(message);
        this.messageId++;
        this.outputInUsageSemaphore = false;

        return result;
    }

    /** Call this as BT is set up and ready for use */
    public boolean BTReady()
    {
        if (this.isClosed || this.outputWaiting || this.outputInUsageSemaphore)
        {
            return false;
        }
        this.outputInUsageSemaphore = true;


        // as we are the server we need to give along a new sessionId as well
        // we just use the current timestamp as this is pretty unique
        // (at least as long as we are not switching event horizon or stuff ...)
        this.sessionId = String.valueOf(Calendar.MILLISECOND);


        // build the message
        String message = BT_READY +
                    CONCAT +
                this.sessionId +
                    CONCAT +
                this.messageId
                ;


        // put message on message stack
        this.messageToSend = message;
        this.outputWaiting = true;
        this.messageId++;
        this.outputInUsageSemaphore = false;

        return true;
    }

    /** Call this as both players are done placing ships and game can start.
     * This will determine by random which player may start and sends a
     * appropriate message to client.
     */
    public boolean InitGameStart()
    {
        /*
        if (this.isClosed || this.outputWaiting || this.outputInUsageSemaphore)
        {
            return false;
        }*/
        this.outputInUsageSemaphore = true;


        String playerStart;
        Random random = new Random(parent.getRandomSeed());
        // well..almost random :)
        if (random.nextInt() % 2 == 0)
        {
            // yay - we start!
            playerStart = START_SERVER;
            this.battleground.SetState(1);
        }
        else
        {
            // damn - opponent starts the game
            playerStart = START_CLIENT;
            this.battleground.SetState(2);
        }


        // build the message
        String message = playerStart +
                    CONCAT +
                this.sessionId +
                    CONCAT
                ;

        this.state = GAMEPLAY;

        boolean result = this.SendMessage(message);
        this.messageId++;
        this.outputInUsageSemaphore = false;

        return result;
    }

    /** Called as the player wants to leave the match */
    public boolean SendQuit()
    {
        if (this.state != GAMEPLAY)
        {
            // BT not yet initialized
            return true;
        }
        /*if (this.isClosed || this.outputWaiting || this.outputInUsageSemaphore)
        {
            return false;
        }*/
        this.outputInUsageSemaphore = true;


        // build the message
        String message = QUIT +
                    CONCAT +
                this.sessionId +
                    CONCAT +
                this.messageId
                ;


        boolean result = this.SendMessage(message);
        this.messageId++;
        this.outputInUsageSemaphore = false;

        return result;
    }

    /** Send message to opponent if his last shot hit.
     * @param shotResult 0 - water
     *                   1 - hit
     *                   2 - hit and sunk
     * @param pos position the shot aimed at
     */
    public boolean SendShotResult(int shotResult, Position pos)
    {
        /*
        if (this.isClosed || this.outputWaiting || this.outputInUsageSemaphore)
        {
            if (this.outputWaiting)
            {
                System.out.println("output occupied");
            }
            if (this.outputInUsageSemaphore)
            {
                System.out.println("semaphore occupied");
            }
            return false;
        }
         * */
        this.outputInUsageSemaphore = true;


        String hitString = "";
        switch (shotResult)
        {
            case 0:
                hitString = WATER;
                break;
            case 1:
                hitString = HIT;
                break;
            case 2:
                hitString = SUNK;
                break;
        }


        // build the message
        String message = hitString +
                    CONCAT +
                this.sessionId +
                    CONCAT +
                this.messageId +
                    CONCAT +
                pos.X() +
                    CONCAT +
                pos.Y()
                ;


        boolean result = this.SendMessage(message);
        this.messageId++;
        this.outputInUsageSemaphore = false;

        return result;
    }

    /** Decodes a incoming message and reacts properly */
    public void InterpreteIncomingMessage(String message)
    {
        if (message == null || message.length() < 3)
        {
            return;
        }

        String messageType;
        String obtainedSessionId;
        int concatIndex, oldConcatIndex;


        // -- read MessageType --
        concatIndex = message.indexOf(CONCAT);
        if (concatIndex == -1)
        {
            return;
        }
        messageType = message.substring(0, concatIndex);
        System.out.print("reading message. type: " + messageType);
        // -----------------------------


        // -- read sessionID --
        oldConcatIndex = concatIndex + 1;
        concatIndex = message.indexOf(CONCAT, oldConcatIndex);
        if (concatIndex == -1)
        {
            return;
        }
        obtainedSessionId = message.substring(oldConcatIndex, concatIndex);
        //System.out.print(" sessionID: " + obtainedSessionId);

        if (messageType.equals(BT_READY))
        {
            // opponent' BT is ready - actually this doesn't interest the server
            return;
        }
        else
        {
            // check if this is the vaild sessionId
            if (!obtainedSessionId.equals(this.sessionId))
            {
                System.err.println("invalid sessionID: " + obtainedSessionId +
                        " expected: " + this.sessionId);
                return;
            }
        }

        if (messageType.equals(PLACEMENT_READY))
        {
            // opponent has placed his ships
            this.opponentReady = true;
            if (this.playerReady)
            {
                // we are ready too - start the game by determining who begins
                while (!this.InitGameStart())
                {
                    try {this.wait(500);}
                    catch (Exception e) {}
                }
            }
        }
        else if (messageType.equals(QUIT))
        {
            // shut down BT
            this.parent.OpponentQuit();
            this.isClosed = true;
            return;
        }
        // -----------------------

        // -- read message ID --
        oldConcatIndex = concatIndex + 1;
        concatIndex = message.indexOf(CONCAT, oldConcatIndex);
        if (concatIndex == -1)
        {
            return;
        }
        System.out.print("message ID: " + message.substring(oldConcatIndex, concatIndex));
        // ----------------------

        // -- read coordinates --
        int x, y;
        oldConcatIndex = concatIndex + 1;
        concatIndex = message.indexOf(CONCAT, oldConcatIndex);
        if (concatIndex == -1)
        {
            System.err.println("failed reading X coord");
            return;
        }
        x = Integer.parseInt(message.substring(oldConcatIndex, concatIndex));

        // read the rest
        y = Integer.parseInt(message.substring(concatIndex + 1, message.length()));
        System.out.println("coords: " + x + y);
        // ----------------------


        // -- act properly --
        if (messageType.equals(SHOOT))
        {
            // wait until BG is ready
            while (!this.battleground.readyToReceiveIncoming)
            {
                try {this.wait(500);}
                catch(Exception e){e.printStackTrace();}
            }
            // shoot!
            this.battleground.EnemyShot(new Position(x, y));
        }
        else if (messageType.equals(WATER))
        {
            this.battleground.ReceiveShotResult(0);
        }
        else if (messageType.equals(HIT))
        {
            this.battleground.ReceiveShotResult(1);
        }
        else if (messageType.equals(SUNK))
        {
            this.battleground.ReceiveShotResult(2);
        }
        else
        {
            // whops - something went wrong
            System.err.println("Failed identifying messageType: " + messageType);
            return;
        }
        // ------------------------
    }

    /** Call this as player is done placing his ships. If opponent is ready
     * too game will start. */
    public void PlacementDone()
    {
        this.playerReady = true;
        if (this.opponentReady)
        {
            System.out.println("both ready - trying to start game...");
            while (!this.InitGameStart())
            {
                try {this.wait(500);}
                catch (Exception e) {}
            }
        }
        else
        {
           System.out.println("opponent not yet ready");
        }
    }

    /** ID of the next message to be send. Currently this is correctly written
     * but never read and just existing as a potential problem solver if
     * it shows that BT is unrealiable in message delivery. In that case a list
     * of messageIDs which we wait for approval for must exist.
     *
     * NOTE: As ID is incremented after building the message-string this ID
     * is the increment of the message, currently in the OutputStream.
     * The ID may change on a higher ID incoming on the InputStream, too.
     */
    int messageId = 0;
    // -------------------------------------------------------------------------

    /**
     * Constructs the bluetooth server, but it is initialized
     * in the different thread to "avoid dead lock".
     */
    BTServer (GameInstance parent) {
        this.parent = parent;

        // we have to initialize a system in different thread...
        accepterThread = new Thread (this);
        accepterThread.start ();
    }

    /**
     * Accepts a new client and send him/her a requested image.
     */
    public void run () {
        boolean isBTReady = false;

        try {
            // create/get a local device
            localDevice = LocalDevice.getLocalDevice ();

            // set we are discoverable
            if (!localDevice.setDiscoverable (DiscoveryAgent.GIAC)) {
                // Some implementations always return false, even if
                // setDiscoverable successful
                // throw new IOException("Can't set discoverable mode...");
            }

            // prepare a URL to create a notifier
            StringBuffer url = new StringBuffer ("btspp://");

            // indicate this is a server
            url.append ("localhost").append (':');

            // add the UUID to identify this service
            url.append (BATTLESHIPS_SERVER_UUID.toString ());

            // add the name for our service
            url.append (";name=Picture Server");

            // request all of the client not to be authorized
            // some devices fail on authorize=true
            url.append (";authorize=false");

            // create notifier now
            notifier = (StreamConnectionNotifier) Connector.open (url.toString ());

            // and remember the service record for the later updates

            // remember we've reached this point.
            isBTReady = true;
        }
        catch (Exception e) {
            System.err.println ("Can't initialize bluetooth: " + e);
        }

        // not needed?
        //parent.BTReady(isBTReady);

        // nothing to do if no bluetooth available
        if (!isBTReady) {
            return;
        }

        // ok, start processor now
        processor = new ClientProcessor ();

        // ok, start accepting connections then
        while (!isClosed) {
            this.connection = null;

            try {
                System.out.println("accentAndOpen started");
                this.connection = notifier.acceptAndOpen ();
                System.out.println("accentAndOpen finished");
            }
            catch (IOException e) {
                // wrong client or interrupted - continue anyway
                continue;
            }


            try
            {
                input = this.connection.openInputStream();
                output = this.connection.openOutputStream();
            }
            catch (Exception e)
            {
                System.err.print(e);
            }


            if (this.connection != null)
            {
                this.parent.BTReady();
                while(!this.BTReady())
                {
                    try {this.wait(500);}
                    catch(Exception e) {}
                }
                break;
            }
        }

        // read / write messages
        while(!isClosed)
        {
            //System.out.println("new BTcheck round SERVER..");
            synchronized(this)
            {
                try
                {
                    this.wait(200);
                }
                catch (Exception e)
                {
                    System.err.println(e);
                }
            }
            //System.out.println("after wait ..");
            if (this.outputWaiting)
            {
                if (this.SendMessage(this.messageToSend))
                {
                    // message successfully transmitted
                    this.outputWaiting = false;
                }
            }
            this.ReceiveMessage();
        }
        System.out.println("shutting down server");
    }
    OutputStream output;
    InputStream input;

    boolean outputWaiting = false;
    String messageToSend = "";//MESSAGE FROM SERVER";

    public boolean RequestMessageSend(String message)
    {
        if (this.outputWaiting)
        {
            // another message already waits to be sended - deny request
            return false;
        }
        else
        {
            this.outputWaiting = true;
            this.messageToSend = message;
            return true;
        }
    }
    int count = 0;

    private boolean SendMessage(String message)
    {
        System.out.println("server trying to send: " + message);
        // then open a steam and write a name
        try {
            System.out.print("writing length ... ");
            output.write (message.length () >> 8); // first length byte
            output.write (message.length() & 0xff);
            System.out.println("done!");

            System.out.print("writing data ... ");
            output.write (message.getBytes ());
            System.out.println("done!");

            System.out.print("flushing output ... ");
            output.flush ();
            //output.close();
            System.out.println("done!");
            //out.close ();
        }
        catch (IOException e) {
            System.err.println ("Sending message failed: " + message + e.getMessage());
            return false;
        }

        System.out.println("succeeded!");
        return true;
    }

    private void ReceiveMessage()
    {
        // at very first check if something is available on input stream
        try
        {
            if (input.available() == 0)
            {
                return;
            }
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }


        // then read length of message (which may be unuequal to current
        // number of digits in the inputstream as some digits may be 'on the way'
        int length;
        try
        {
            length = input.read() << 8;
            length |= input.read();
        }
        catch (Exception e)
        {
            System.out.println("failed to read LENGTH from input stream");
            return;
        }

        if (length <= 0)
        {
            return;
        }

        System.out.println("reading message of length: " + String.valueOf(length));


        // read data
        byte[] buffer = new byte[length];
        length = 0;
        try
        {
            while (length != buffer.length) {
                int n = input.read (buffer, length, buffer.length - length);

                if (n == -1) {
                    throw new IOException ("failed to read message");
                }

                length += n;
            }

            // write message to input buffer
            String messageString = new String(buffer /*, "utf-8"*/);
            System.out.println("read BT message: " + messageString);
            this.InterpreteIncomingMessage(messageString);
            //this.parent.receivedMessages.addElement(messageString);

            //input.close ();
        }
        catch (IOException e)
        {
            System.err.println ("Reading message failed");
            return;
        }
    }

    /**
     * Destroy a work with bluetooth - exits the accepting
     * thread and close notifier.
     */
    void destroy () {
        System.out.println("shutting down server");
        isClosed = true;

        // finalize notifier work
        if (notifier != null) {
            try {
                notifier.close ();
            }
            catch (IOException e) {
            } // ignore
        }

        // wait for acceptor thread is done
        try {
            accepterThread.join ();
        }
        catch (InterruptedException e) {
        } // ignore

        // finalize processor
        if (processor != null) {
            processor.destroy (true);
        }

        processor = null;
    }

    /**
     * Reads the image name from the specified connection
     * and sends this image through this connection, then
     * close it after all.
     */
    /*
    private void processConnection (StreamConnection conn) {
        // read the image name first
        String imgName = readImageName (conn);

        // check this image is published and get the image file name
        imgName = "bla"; //parent.getImageFileName (imgName);

        // load image data into buffer to be send
        byte[] imgData = getImageData (imgName);

        // send image data now
        sendImageData (imgData, conn);

        // close connection and good-bye
        try {
            conn.close ();
        }
        catch (IOException e) {
        } // ignore
    }
     * */

    /**
     * Send image data.
     */
    /*
    private void sendImageData (byte[] imgData, StreamConnection conn) {
        if (imgData == null) {
            return;
        }

        OutputStream out = null;
        System.out.println("sending ...");

        try {
            out = conn.openOutputStream ();
            out.write (imgData.length >> 8);
            out.write (imgData.length & 0xff);
            out.write (imgData);
            out.flush ();
        }
        catch (IOException e) {
            System.err.println ("Can't send image data: " + e);
        }

        // close output stream anyway
        if (out != null) {
            try {
                out.close ();
            }
            catch (IOException e) {
            } // ignore
        }
    }
     * */

    /**
     * Reads image name from specified connection.
     */
    /*
    private String readImageName (StreamConnection conn) {
        String imgName = null;
        InputStream in = null;
System.out.println("reading");
        try {
            in = conn.openInputStream ();

            int length = in.read (); // 'name' length is 1 byte

            if (length <= 0) {
                throw new IOException ("Can't read name length");
            }

            byte[] nameData = new byte[length];
            length = 0;

            while (length != nameData.length) {
                int n = in.read (nameData, length, nameData.length - length);

                if (n == -1) {
                    throw new IOException ("Can't read name data");
                }

                length += n;
            }

            imgName = new String (nameData);
        }
        catch (IOException e) {
            System.err.println (e);
        }

        // close input stream anyway
        if (in != null) {
            try {
                in.close ();
            }
            catch (IOException e) {
            } // ignore
        }

        return imgName;
    }
     * */

    /**
     * Reads images data from MIDlet archive to array.
     */
    /*
    private byte[] getImageData (String imgName) {
        System.out.println("getting");
        if (imgName == null) {
            return null;
        }

        InputStream in = getClass ().getResourceAsStream (imgName);

        // read image data and create a byte array
        byte[] buff = new byte[1024];
        ByteArrayOutputStream baos = new ByteArrayOutputStream (1024);

        try {
            while (true) {
                int length = in.read (buff);

                if (length == -1) {
                    break;
                }

                baos.write (buff, 0, length);
            }
        }
        catch (IOException e) {
            System.err.println ("Can't get image data: imgName=" + imgName + " :" + e);

            return null;
        }

        return baos.toByteArray ();
    }
     * */

    /**
     * Organizes the queue of clients to be processed,
     * processes the clients one by one until destroyed.
     */
    private class ClientProcessor implements Runnable {
        private Thread processorThread;
        private Vector queue = new Vector ();

        ClientProcessor () {
            processorThread = new Thread (this);
            processorThread.start ();
        }

        public void run () {
            System.out.println("running client processor");
            while (!isClosed) {
                // wait for new task to be processed
                synchronized (this) {
                    if (queue.size () == 0) {
                        try {
                            wait ();
                        }
                        catch (InterruptedException e) {
                            System.err.println ("Unexpected exception: " + e);
                            destroy (false);

                            return;
                        }
                    }
                }

                // send the image to specified connection
                StreamConnection conn;

                synchronized (this) {
                    // may be awaked by "destroy" method.
                    if (isClosed) {
                        return;
                    }

                    conn = (StreamConnection) queue.firstElement ();
                    queue.removeElementAt (0);
                    //processConnection (conn);
                }
            }
        }

        /**
         * Adds the connection to queue and notifies the thread.
         * @param connection connection
         */
        void addConnection (StreamConnection connection) {
            System.out.println("adding");
            synchronized (this) {
                queue.addElement (connection);
                notify ();
            }
        }

        /**
         * Closes the connections and .
         */
        void destroy (boolean needJoin) {
            StreamConnection conn;

            synchronized (this) {
                notify ();

                while (queue.size () != 0) {
                    conn = (StreamConnection) queue.firstElement ();
                    queue.removeElementAt (0);

                    try {
                        conn.close ();
                    }
                    catch (IOException e) {
                    } // ignore
                }
            }

            // wait until dispatching thread is done
            try {
                processorThread.join ();
            }
            catch (InterruptedException e) {
            } // ignore
        }
    }
}

